8.4.2 整合并完成工作

这个例子展示标准库里不同包是如何通过支持实现了 io.Writer 接口类型的值来一起完成工作的。这个示例里使用了 bytesfmtos 包来进行缓冲、拼接和写字符串到 stdout ,如代码清单8-37所示。

代码清单8-37 listing37.go

01 // 这个示例程序展示来自不同标准库的不同函数是如何
02 // 使用io.Writer接口的
03 package main
04
05 import (
06   "bytes"
07   "fmt"
08   "os"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建一个Buffer值,并将一个字符串写入Buffer
14   // 使用实现io.Writer的Write方法
15   var b bytes.Buffer
16   b.Write([]byte("Hello "))
17
18   // 使用Fprintf来将一个字符串拼接到Buffer里
19   // 将bytes.Buffer的地址作为io.Writer类型值传入
20   fmt.Fprintf(&b, "World!")
21
22   // 将Buffer的内容输出到标准输出设备
23   // 将os.File值的地址作为io.Writer类型值传入
24   b.WriteTo(os.Stdout)
25 }

运行代码清单8-37中的程序会得到代码清单8-38所示的输出。

代码清单8-38 listing37.go的输出

Hello World!

这个程序使用了标准库的3个包来将 "Hello World!" 输出到终端窗口。一开始,程序在第15行声明了一个 bytes 包里的 Buffer 类型的变量,并使用零值初始化。在第16行创建了一个 byte 切片,并用字符串 "Hello" 初始化了这个切片。 byte 切片随后被传入 Write 方法,成为 Buffer 类型变量里的初始内容。

第20行使用 fmt 包里的 Fprintf 函数将字符串 "World!" 追加到 Buffer 类型变量里。让我们看一下 Fprintf 函数的声明,如代码清单8-39所示。

代码清单8-39 golang.org/src/fmt/print.go

// Fprintf根据格式化说明符来格式写入内容,并输出到w
// 这个函数返回写入的字节数,以及任何遇到的错误
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)

需要注意 Fprintf 函数的第一个参数。这个参数需要接收一个实现了 io.Writer 接口类型的值。因为我们传入了之前创建的 Buffer 类型值的地址,这意味着 bytes 包里的 Buffer 类型必须实现了这个接口。那么在 bytes 包的源代码里,我们应该能找到为 Buffer 类型声明的 Write 方法,如代码清单8-40所示。

代码清单8-40 golang.org/src/bytes/buffer.go

// Write将p的内容追加到缓冲区,如果需要,会增大缓冲区的空间。返回值n是
// p的长度,err总是nil。如果缓冲区变得太大,Write会引起崩溃…
func (b *Buffer) Write(p []byte) (n int, err error) {
  b.lastRead = opInvalid
  m := b.grow(len(p))
  return copy(b.buf[m:], p), nil
}

代码清单8-40展示了 Buffer 类型的 Write 方法的当前版本的实现。由于实现了这个方法,指向 Buffer 类型的指针就满足了 io.Writer 接口,可以将指针作为第一个参数传入 Fprintf 。在这个例子里,我们使用 Fprintf 函数,最终通过 Buffer 实现的 Write 方法,将 "World!" 字符串追加到 Buffer 类型变量的内部缓冲区。

让我们看一下代码清单8-37的最后几行,如代码清单8-41所示,将整个 Buffer 类型变量的内容写到 stdout

代码清单8-41 listing37.go:第22行到第25行

22 // 将Buffer的内容输出到标准输出设备
23   // 将os.File值的地址作为io.Writer类型值传入
24   b.WriteTo(os.Stdout)
25 }

在代码清单8-37的第24行,使用 WriteTo 方法将 Buffer 类型的变量的内容写到 stdout 设备。这个方法接受一个实现了 io.Writer 接口的值。在这个程序里,传入的值是 os 包的 Stdout 变量的值,如代码清单8-42所示。

代码清单8-42 golang.org/src/os/file.go

var (
  Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
  Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
  Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

这些变量自动声明为 NewFile 函数返回的类型,如代码清单8-43所示。

代码清单8-43 golang.org/src/os/file_unix.go

// NewFile返回一个具有给定的文件描述符和名字的新File
func NewFile(fd uintptr, name string) *File {
  fdi := int(fd)
  if fdi < 0 {
    return nil
  }
  f := &File{&file{fd: fdi, name: name}}
  runtime.SetFinalizer(f.file, (*file).close)
  return f
}

就像在代码清单8-43里看到的那样, NewFile 函数返回一个指向 File 类型的指针。这就是 Stdout 变量的类型。既然我们可以将这个类型的指针作为参数传入 WriteTo 方法,那么这个类型一定实现了 io.Writer 接口。在 os 包的源代码里,我们应该能找到 Write 方法,如代码清单8-44所示。

代码清单8-44 golang.org/src/os/file.go

// Write将len(b)个字节写入File
// 这个方法返回写入的字节数,如果有错误,也会返回错误
// 如果n != len(b),Write会返回一个非nil的错误
func (f *File) Write(b []byte) (n int, err error) {
  if f == nil {
    return 0, ErrInvalid
  }
  n, e := f.write(b)
  if n < 0 {
    n = 0
  }
  if n != len(b) {
    err = io.ErrShortWrite
  }
  epipecheck(f, e)
  if e != nil {
    err = &PathError{"write", f.name, e}
  }
  return n, err
}

没错,代码清单8-44中的代码展示了 File 类型指针实现 io.Writer 接口类型的代码。让我们再看一下代码清单8-37的第24行,如代码清单8-45所示。

代码清单8-45 listing37.go:第22行到第25行

22   // 将Buffer的内容输出到标准输出设备
23   // 将os.File值的地址作为io.Writer类型值传入
24   b.WriteTo(os.Stdout)
25 }

可以看到, WriteTo 方法可以将 Buffer 类型变量的内容写到 stdout ,结果就是在终端窗口上显示了 "Hello World!" 字符串。这个方法会通过接口值,调用 File 类型实现的 Write 方法。

这个例子展示了接口的优雅以及它带给语言的强大的能力。得益于 bytes.Bufferos.File 类型都实现了 Writer 接口,我们可以使用标准库里已有的功能,将这些类型组合在一起完成工作。接下来让我们看一个更加实用的例子。

results matching ""

    No results matching ""